/*
 *   Copyright 2012, Thomas Kerber
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package milk.jpatch.code;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import milk.jpatch.Util;

import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.FieldInstruction;
import org.apache.bcel.generic.GETFIELD;
import org.apache.bcel.generic.GETSTATIC;
import org.apache.bcel.generic.Instruction;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.LocalVariableInstruction;
import org.apache.bcel.generic.PUTFIELD;
import org.apache.bcel.generic.PUTSTATIC;

/**
 * Denotes a code segment initializing a field.
 * @author Thomas Kerber
 * @version 1.0.0
 */
public class FieldInitCodeSegment implements Serializable{
    
    private static final long serialVersionUID = -2308147557332096828L;
    
    /**
     * The instruction list.
     */
    public final InstructionList il;
    /**
     * The fields name.
     */
    public final String fieldName;
    /**
     * The new local variable indexes.
     * 
     * If these indexes are already defined, they are changed in the code.
     */
    // TODO: add something similar to CodeAddDelta.
    public final int[] newLocalVars;
    
    /**
     * 
     * @param il The instruction list.
     * @param fieldName The fields name.
     * @param newLocalVars The new local variable indexes.
     */
    public FieldInitCodeSegment(InstructionList il, String fieldName,
            int[] newLocalVars){
        this.il = il;
        this.fieldName = fieldName;
        this.newLocalVars = newLocalVars;
    }
    
    /**
     * Generates...
     * @param methods The class methods.
     * @param static_ Whether or not to generate static fics.
     * @param cpg The cpool gen.
     * @return The fics.
     */
    public static List<FieldInitCodeSegment> generate(Method[] methods,
            boolean static_, ConstantPoolGen cpg){
        List<FieldInitCodeSegment> ret =
                new ArrayList<FieldInitCodeSegment>();
        
        List<Method> initMethods = new ArrayList<Method>();
        for(Method m : methods){
            if(m.isStatic() != static_)
                continue;
            if((!static_ && m.getName().equals("<init>")) ||
                    (static_ && m.getName().equals("<cinit>")))
                initMethods.add(m);
        }
        List<List<FieldInitCodeSegment>> ficss =
                new ArrayList<List<FieldInitCodeSegment>>();
        for(Method m : initMethods)
            ficss.add(generate(new InstructionList(m.getCode().getCode()),
                    Util.getFixedLocalVariableLength(m), static_, cpg));
        
        // Field names contained in _all_ each methods fics. (The rest can't be
        // equal for every method anyhow and are ignored)
        Set<String> fieldNames = null;
        for(List<FieldInitCodeSegment> ficsL : ficss){
            Set<String> thisMethodFieldNames = new HashSet<String>();
            for(FieldInitCodeSegment fics : ficsL){
                thisMethodFieldNames.add(fics.fieldName);
            }
            if(fieldNames == null)
                fieldNames = thisMethodFieldNames;
            else
                fieldNames.retainAll(thisMethodFieldNames);
        }
        outer: for(String fieldName : fieldNames){
            FieldInitCodeSegment cmpTo = null;
            {
                List<FieldInitCodeSegment> ficsL = ficss.get(0);
                for(FieldInitCodeSegment fics : ficsL){
                    if(fics.fieldName.equals(fieldName)){
                        cmpTo = fics;
                        break;
                    }
                }
            }
            for(int i = 1; i < ficss.size(); i++){
                FieldInitCodeSegment cmpWith = null;
                {
                    List<FieldInitCodeSegment> ficsL = ficss.get(i);
                    for(FieldInitCodeSegment fics : ficsL){
                        if(fics.fieldName.equals(fieldName)){
                            cmpWith = fics;
                            break;
                        }
                    }
                }
                if(!cmpTo.equals(cmpWith))
                    continue outer;
            }
            ret.add(cmpTo);
        }
        
        return ret;
    }
    
    /**
     * Generates...
     * @param il The instruction list to generate from.
     * @param static_ Whether this is <clinit> or <init>.
     * @param cpg The cpool.
     * @return The init code segments
     */
    public static List<FieldInitCodeSegment> generate(InstructionList il,
            int paramCount, boolean static_, ConstantPoolGen cpg){
        List<FieldInitCodeSegment> ret =
                new ArrayList<FieldInitCodeSegment>();
        
        /*
         *  An init code segment is a segment which:
         *  
         *  a) modifies the stack by +- 0.
         * *b) lies in top-level code.
         * *c) ends with a putfield to a static field if static_ is true, or
         *     to a non-static field if static_ is false. The putfield must
         *     be to a field which is not accessed more than once.
         *  d) contains only local variables which are not used elsewhere.
         *  e) does not access any other fields, unless the fields are static
         *     and are accessed in the <init> method.
         *  f) does not contain any jump instructions.
         */
        List<InstructionHandle> topLevelCode = CodeUtil.
                getTopLevelInstructions(il);
        
        InstructionHandle[] ihl = il.getInstructionHandles();
        
        List<Integer> shortlistedPutfields = new ArrayList<Integer>();
        for(int i = 0; i < topLevelCode.size(); i++){
            if(topLevelCode.get(i) == null)
                continue;
            if(topLevelCode.get(i).getInstruction() instanceof PUTFIELD)
                shortlistedPutfields.add(i);
        }
        // Remove of field is accessed more than once.
        for(int i = 0; i < shortlistedPutfields.size(); i++){
            String fieldName =
                    ((PUTFIELD)topLevelCode.get(i).getInstruction()).
                            getFieldName(cpg);
            for(InstructionHandle ih : ihl){
                if(ih == topLevelCode.get(shortlistedPutfields.get(i)))
                    continue;
                Instruction in = ih.getInstruction();
                if(in instanceof FieldInstruction){
                    if(!static_ && (in instanceof GETSTATIC ||
                            in instanceof PUTSTATIC))
                        continue;
                    else if(static_ && (in instanceof GETFIELD ||
                            in instanceof PUTFIELD))
                        continue;
                    if(((FieldInstruction) in).getFieldName(cpg).equals(
                            fieldName)){
                        shortlistedPutfields.remove(i--);
                        break;
                    }
                }
            }
        }
        
        for(int i = 0; i < shortlistedPutfields.size(); i++){
            // Already start one back so as to avoid the last putfield
            // triggering the "no field access" rule.
            
            int pos = shortlistedPutfields.get(i) - 1;
            int endPos = shortlistedPutfields.get(i);
            
            int stackModif = 2;
            
            // Go backwards, adding to the stack. If at any time one of the
            // Conditions f or e are violated, or a null or index -1 is
            // encountered in top level code, this isn't field init code.
            
            // every time stackModif is 0, the condition d is checked, and if
            // successful, this is final and is split off into an instruction
            // list.
            
            outer: while(true){
                if(pos == -1)
                    break;
                InstructionHandle instrH = topLevelCode.get(pos);
                if(instrH == null)
                    break;
                Instruction instr = instrH.getInstruction();
                if(CodeUtil.isJump(instr))
                    break;
                if(instr instanceof FieldInstruction)
                    if(!(!static_ &&
                            (instr instanceof PUTSTATIC ||
                             instr instanceof GETSTATIC)))
                        break;
                stackModif -= CodeUtil.getInstructionStackModif(instr,
                        cpg.getConstantPool());
                if(stackModif == 0){
                    List<Integer> localVariableIndexes =
                            new ArrayList<Integer>();
                    // Gather list of local variable indexes in the block.
                    for(int f = pos; f <= endPos; f++){
                        Instruction instr2 = topLevelCode.get(f).
                                getInstruction();
                        if(instr2 instanceof LocalVariableInstruction){
                            int localIndex = ((LocalVariableInstruction)instr2).
                                    getIndex();
                            // Ignore parameters. (Yes, even though they can
                            // theoretically be edited. That may induce possible
                            // errors, but is far more convinient)
                            if(localIndex < paramCount)
                                continue;
                            localVariableIndexes.add(localIndex);
                        }
                    }
                    // And compare against the entire method code.
                    icmp: for(InstructionHandle ih : ihl){
                        // Ignore handles in the block.
                        for(int f = pos; f <= endPos; f++)
                            if(topLevelCode.get(f) == ih)
                                continue icmp;
                        Instruction instr2 = ih.getInstruction();
                        if(!(instr2 instanceof LocalVariableInstruction))
                            continue;
                        int localIndex = ((LocalVariableInstruction)instr2).
                                getIndex();
                        for(Integer blockIndex : localVariableIndexes)
                            if(blockIndex == localIndex)
                                // Jackpot. If won't work.
                                continue outer;
                    }
                    // If it made it through: congratulations! Your
                    // FieldInitCodeSegment may now be generated!
                    InstructionList newList = new InstructionList();
                    for(int f = pos; f <= endPos; f++){
                        newList.append(topLevelCode.get(f).getInstruction());
                    }
                    String fieldName = ((FieldInstruction)topLevelCode.
                            get(endPos).getInstruction()).getFieldName(cpg);
                    int[] localVariableArr = Util.toIntArray(
                            localVariableIndexes);
                    ret.add(new FieldInitCodeSegment(newList, fieldName,
                            localVariableArr));
                }
            }
        }
        
        return ret;
    }
    
    @Override
    public boolean equals(Object o){
        if(o == null)
            return false;
        if(this == o)
            return true;
        if(!(o instanceof FieldInitCodeSegment))
            return false;
        FieldInitCodeSegment other = (FieldInitCodeSegment)o;
        if(!this.fieldName.equals(other.fieldName))
            return false;
        if(this.il.size() != other.il.size())
            return false;
        Instruction[] ins1 = il.getInstructions();
        Instruction[] ins2 = other.il.getInstructions();
        for(int i = 0; i < il.size(); i++){
            if(!(ins1[i] instanceof LocalVariableInstruction &&
                    ins2[i] instanceof LocalVariableInstruction)){
                if(!ins1[i].equals(ins2[i]))
                    return false;
            }
            else{
                LocalVariableInstruction lvi1 =
                        (LocalVariableInstruction)ins1[i];
                LocalVariableInstruction lvi2 =
                        (LocalVariableInstruction)ins2[i];
                int lvindex1 = lvi1.getIndex();
                int lvindex2 = lvi2.getIndex();
                // Indexes get replaced with their index in the newLocalVars
                // array. That way, the same local vars are matched, even if
                // they have a different index.
                int nlvindex1 = -1;
                int nlvindex2 = -1;
                for(int f = 0; f < newLocalVars.length; f++){
                    if(newLocalVars[f] == lvindex1){
                        nlvindex1 = f;
                        break;
                    }
                }
                for(int f = 0; f < other.newLocalVars.length; f++){
                    if(other.newLocalVars[f] == lvindex2){
                        nlvindex2 = f;
                        break;
                    }
                }
                if(nlvindex1 != nlvindex2)
                    return false;
                lvi1.setIndex(nlvindex1);
                lvi2.setIndex(nlvindex2);
                boolean eq = lvi1.toString().equals(lvi2.toString());
                lvi1.setIndex(lvindex1);
                lvi2.setIndex(lvindex2);
                if(!eq)
                    return false;
            }
        }
        return true;
    }
    
}
